home *** CD-ROM | disk | FTP | other *** search
/ Clickx 115 / Clickx 115.iso / software / tools / windows / tails-i386-0.16.iso / live / filesystem.squashfs / usr / share / pyshared / whisperBack / whisperback.py < prev   
Encoding:
Python Source  |  2012-12-13  |  10.8 KB  |  285 lines

  1. #!/usr/bin/env python
  2. # -*- coding: UTF-8 -*-
  3.  
  4. ########################################################################
  5. # WhisperBack - Send feedback in an encrypted mail
  6. # Copyright (C) 2009-2012 Tails developers <amnesia.org>
  7. #
  8. # This file is part of WhisperBack
  9. #
  10. # WhisperBack is  free software; you can redistribute  it and/or modify
  11. # it under the  terms of the GNU General Public  License as published by
  12. # the Free Software Foundation; either  version 3 of the License, or (at
  13. # your option) any later version.
  14. #
  15. # This program  is distributed in the  hope that it will  be useful, but
  16. # WITHOUT   ANY  WARRANTY;   without  even   the  implied   warranty  of
  17. # MERCHANTABILITY  or FITNESS  FOR A  PARTICULAR PURPOSE.   See  the GNU
  18. # General Public License for more details.
  19. #
  20. # You should have received a copy of the GNU General Public License
  21. # along with this program.  If not, see <http://www.gnu.org/licenses/>.
  22. ########################################################################
  23.  
  24. """WhisperBack main backend
  25.  
  26. """
  27.  
  28. import gobject
  29. # Workaround an API change: timeout_add was moved from gobject to glib
  30. # in 2.16
  31. #pylint: disable=C0301,C0111,C0103,W0232,R0903
  32. if gobject.pygobject_version[:2] >= (2, 16):
  33.     import glib
  34. else:
  35.     class glib:
  36.         timeout_add = gobject.timeout_add
  37. gobject.threads_init()
  38.  
  39. import os
  40. import threading
  41.  
  42. # Import our modules
  43. import whisperBack.exceptions
  44. import whisperBack.mail
  45. import whisperBack.encryption
  46. import whisperBack.utils
  47.  
  48. # pylint: disable=R0902
  49. class WhisperBack(object):
  50.     """
  51.     This class contains the backend which actually sends the feedback
  52.     """
  53.     
  54.     def set_contact_email(self, email):
  55.         """Sets an optional email address to be used for furether communication
  56.  
  57.         """
  58.         if whisperBack.utils.is_valid_email(email):
  59.             self._contact_email = email
  60.         else:
  61.  
  62.             #XXX use a better exception
  63.             raise ValueError, _("Invalid contact email: %s" % email)
  64.  
  65.     #pylint: disable=W0212
  66.     contact_email = property(lambda self: self._contact_email,
  67.                              set_contact_email)
  68.  
  69.     def set_contact_gpgkey(self, gpgkey):
  70.         """Sets an optional PGP key to be used for furether communication
  71.  
  72.         """
  73.         if (whisperBack.utils.is_valid_pgp_block(gpgkey) or
  74.             whisperBack.utils.is_valid_pgp_id(gpgkey) or
  75.             whisperBack.utils.is_valid_link(gpgkey)):
  76.             self._contact_gpgkey = gpgkey
  77.         else:
  78.             #XXX use a better exception
  79.             if len(gpgkey.splitlines()) <= 1:
  80.                 message = _("Invalid contact OpenPGP key: %s" % gpgkey)
  81.             else:
  82.                 message = _("Invalid contact OpenPGP public key block")
  83.             raise ValueError, message
  84.  
  85.     #pylint: disable=W0212
  86.     contact_gpgkey = property(lambda self: self._contact_gpgkey,
  87.                                set_contact_gpgkey)
  88.  
  89.     def __init__(self, subject = "", message = ""):
  90.         """Initialize a feedback object with the given contents
  91.         
  92.         @param subject The topic of the feedback 
  93.         @param message The content of the feedback
  94.         """
  95.         self.__thread = None
  96.         self.__error_output = None
  97.  
  98.         # Initialize config variables
  99.         self.html_help = ""
  100.         self.gnupg_keyring = None
  101.         self.to_address = None
  102.         self.to_fingerprint = None
  103.         self.from_address = None
  104.         self.mail_prepended_info = lambda: ""
  105.         self.mail_appended_info = lambda: ""
  106.         self.mail_subject = None
  107.         self.smtp_host = None
  108.         self.smtp_port = None
  109.         self.smtp_tlscafile = None
  110.  
  111.         # Load the python configuration file "config.py" from diffrents locations
  112.         # XXX: this is an absolute path, bad !
  113.         self.__load_conf(os.path.join("/", "etc", "whisperback", "config.py"))
  114.         self.__load_conf(os.path.join(os.path.expanduser('~'),
  115.                                       ".whisperback",
  116.                                       "config.py"))
  117.         self.__load_conf(os.path.join(os.getcwd(), "config.py"))
  118.         self.__check_conf()
  119.  
  120.         # Get additional info through the callbacks and sanitize it
  121.         self.prepended_data = whisperBack.utils.sanitize_hardware_info(self.mail_prepended_info())
  122.         self.appended_data = whisperBack.utils.sanitize_hardware_info(self.mail_appended_info())
  123.  
  124.         # Initialize other variables
  125.         self.subject = subject
  126.         self.message = message
  127.         self._contact_email = None
  128.         self._contact_gpgkey = None
  129.         self.send_attempts = 0
  130.  
  131.     def __load_conf(self, config_file_path):
  132.         """Loads a configuration file from config_file_path and executes it
  133.         inside the current class.
  134.         
  135.         @param config_file_path The path on the configuration file to load
  136.         """
  137.  
  138.         f = None
  139.         try:
  140.             f = open(config_file_path, 'r')
  141.             code = f.read()
  142.         except IOError:
  143.             # There's no problem if one of the configuration files is not
  144.             # present
  145.             return None
  146.         finally:
  147.             if f:
  148.                 f.close()
  149.         #pylint: disable=W0122
  150.         exec code in self.__dict__
  151.  
  152.     def __check_conf(self):
  153.         """Check that all the required configuration variables are filled
  154.         and raise MisconfigurationException if not.
  155.         """
  156.  
  157.         # XXX: Add sanity checks
  158.         
  159.         if not self.to_address:
  160.             raise whisperBack.exceptions.MisconfigurationException('to_address')
  161.         if not self.to_fingerprint:
  162.             raise whisperBack.exceptions.MisconfigurationException('to_fingerprint')
  163.         if not self.from_address:
  164.             raise whisperBack.exceptions.MisconfigurationException('from_address')
  165.         if not self.mail_subject:
  166.             raise whisperBack.exceptions.MisconfigurationException('mail_subject')
  167.         if not self.smtp_host:
  168.             raise whisperBack.exceptions.MisconfigurationException('smtp_host')
  169.         if not self.smtp_port:
  170.             raise whisperBack.exceptions.MisconfigurationException('smtp_port')
  171.         if not self.smtp_tlscafile:
  172.             raise whisperBack.exceptions.MisconfigurationException('smtp_tlscafile')
  173.  
  174.     def execute_threaded(self, func, args, progress_callback=None, 
  175.                            finished_callback=None, polling_freq=100):
  176.         """Execute a function in another thread and handle it.
  177.         
  178.         Execute the function `func` with arguments `args` in another thread,
  179.         and poll whether the thread is alive, executing the callback
  180.         `progress_callback` every `polling_frequency`. When the function
  181.         thread terminates, saves the execption it eventually raised and pass
  182.         it to `finished_callback`.
  183.         
  184.         @param func               the function to execute.
  185.         @param args               the tuple to pass as arguments to `func`.
  186.         @param progress_callback  (optional) a callback function to call
  187.                                   every time the execution thread is polled.
  188.                                   It doesn't take any agument. 
  189.         @param finished_callback  (optional) a callback function to call when
  190.                                   the execution thread terminated. It receives
  191.                                   the exception raised by `func`, if any, or
  192.                                   None.
  193.         @param polling_freq       (optional) the interal between polling
  194.                                   iterations (in ms).
  195.         """
  196.         #pylint: disable=C0111
  197.         def save_exception(func, args):
  198.             try:
  199.                 #pylint: disable=W0142
  200.                 func(*args)
  201.             except Exception, e:
  202.                 self.__error_output = e
  203.                 raise
  204.  
  205.         def poll_thread(self):
  206.             if progress_callback is not None:
  207.                 progress_callback()
  208.             if self.__thread.isAlive():
  209.                 return True
  210.             else:
  211.                 if finished_callback is not None:
  212.                     finished_callback(self.__error_output)
  213.                 return False
  214.  
  215.         self.__error_output = None
  216.         assert self.__thread is None or not self.__thread.isAlive()
  217.         self.__thread = threading.Thread(target=save_exception, args=(func, args))
  218.         self.__thread.start()
  219.         # XXX: there could be no main loop
  220.         glib.timeout_add(polling_freq, poll_thread, self)
  221.     # XXX: static would be best, but I get a problem with self.*
  222.     #execute_threaded = staticmethod(execute_threaded)
  223.  
  224.     def get_message_body(self):
  225.         """Returns the content of the message body
  226.  
  227.         Aggregate all informations to prepare the message body.
  228.         """
  229.         body = "Subject: %s\n" % self.subject
  230.         if self.contact_email:
  231.             body += "From: %s\n" % self.contact_email
  232.         if self.contact_gpgkey:
  233.             # Test whether we have a key block or a key id/url
  234.             if len(self.contact_gpgkey.splitlines()) <= 1:
  235.                 body += "OpenPGP-Key: %s\n" % self.contact_gpgkey
  236.             else:
  237.                 body += "OpenPGP-Key: included below\n"
  238.         body += "%s\n%s\n\n" % (self.prepended_data, self.message)
  239.         if self.contact_gpgkey and len(self.contact_gpgkey.splitlines()) > 1:
  240.             body += "%s\n\n" % self.contact_gpgkey
  241.         body += "%s\n" % self.appended_data
  242.         return body
  243.  
  244.     def get_encrypted_message_body(self):
  245.         """Returns the encrypted body of the email to be send"""
  246.  
  247.         encryption = whisperBack.encryption.Encryption(keyring=self.gnupg_keyring)
  248.         return encryption.encrypt(self.get_message_body(), [self.to_fingerprint])
  249.  
  250.     def save(self, path):
  251.         """Save the message into a file
  252.  
  253.         @param path path of the file to save
  254.         """
  255.         f = open(path, 'w')
  256.         try:
  257.             f.write(self.get_encrypted_message_body())
  258.         finally:
  259.             f.close()
  260.  
  261.     def send(self, progress_callback=None, finished_callback=None):
  262.         """Actually sends the message
  263.         
  264.         @param progress_callback 
  265.         @param finished_callback
  266.         """
  267.         
  268.         # XXX: It's really strange that some exceptions from this method are
  269.         #      raised and some other transmitted to finished_callbackΓǪ
  270.  
  271.         self.send_attempts = self.send_attempts + 1
  272.         
  273.         encrypted_message_body = self.get_encrypted_message_body()
  274.  
  275.         mime_message = whisperBack.mail.create_message(self.from_address,
  276.                                         self.to_address, self.mail_subject,
  277.                                         encrypted_message_body)
  278.  
  279.         self.execute_threaded(func=whisperBack.mail.send_message_tls,
  280.                               args=(self.from_address, self.to_address,
  281.                                     mime_message, self.smtp_host,
  282.                                     self.smtp_port, self.smtp_tlscafile),
  283.                               progress_callback=progress_callback,
  284.                               finished_callback=finished_callback)
  285.